package org.six11.olive3.picturephone.server.handler;

import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;

import javax.servlet.ServletException;

import org.six11.olive3.net.Message;
import org.six11.olive3.net.MessageHandler;
import org.six11.olive3.picturephone.server.Find;
import org.six11.olive3.picturephone.server.PPMessageNames;
import org.six11.olive3.picturephone.server.TodoBatch;
import org.six11.olive3.server.DatabaseThing;
import org.six11.util2.Debug;

/**
 * 
 * 
 * @author Gabe Johnson <johnsogg@cmu.edu>
 */
public class AnythingRequestHandler extends MessageHandler {

  private RateRequestMessageHandler rateReqHandler;
  private SketchRequestMessageHandler sketchReqHandler;
  private DescriptionRequestMessageHandler descReqHandler;
  private DatabaseThing dbThing;
  public static double RATE_PROBABILITY = 0.7;

  // private TodoBatch todo;
  Find finder;
  private static Random entropy = new Random(System.currentTimeMillis());

  public AnythingRequestHandler(RateRequestMessageHandler rateReqHandler,
      SketchRequestMessageHandler sketchReqHandler,
      DescriptionRequestMessageHandler descReqHandler, DatabaseThing dbThing) {
    this.rateReqHandler = rateReqHandler;
    this.sketchReqHandler = sketchReqHandler;
    this.descReqHandler = descReqHandler;
    this.dbThing = dbThing;
    finder = new Find(dbThing);
  }

  public void handleNow(Message m, PrintWriter out) {

    String uid = m.getParamValue(PPMessageNames.PARAM_USER_ID);
    boolean rateOK = "true".equals(m.getParamValue(PPMessageNames.PARAM_PREF_RATE));
    boolean sketchOK = "true".equals(m.getParamValue(PPMessageNames.PARAM_PREF_SKETCH));
    boolean descOK = "true".equals(m.getParamValue(PPMessageNames.PARAM_PREF_DESC));

    // pick something the player can (and hopefully wants to) use, and invoke the appropriate
    // handler.
    try {
      bug("Beginning findSomething while loop.");
      while (!findSomething(uid, rateOK, sketchOK, descOK, out)) {
        bug("trying again...");
      }
      bug("I must have found something!");
    } catch (Exception ex) {
      // exception is thrown in there isn't anything the user can do.
      ex.printStackTrace();
      Message error = new Message(PPMessageNames.MSG_ERROR);
      error.addParam(PPMessageNames.PARAM_TEXT, "Could not find any tasks for user " + uid
          + ". Sorry. " + //
          "This means you have done everything you can do to " + //
          "the PicturePhone database. More likely than not this is an error and you " + //
          "should email Gabe (johnsogg@cmu.edu). But if you play all the time it might " + //
          "be legitimate. I dunno. If this isn't an error, check back soon, since other " + //
          "(less industrious) players are adding new content all the time.");
      out.write(error.toOliveString());
    }

    finder.findAllToDoInBackground(uid);
  }

  /**
   * This method tries to find something for the user do to based on the supplied activitiy
   * preferences and the current state of the various todo_* database tables. It picks one thing and
   * attempts give it to the user. However, the items from the todo_* tables are not always
   * up-to-date, so the attempt may fail. In case of failure it will clean up the failing item in
   * the database and return false. If all goes well, it returns true.
   * 
   * @param uid
   * @param rateOK
   * @param sketchOK
   * @param descOK
   * @param out
   * @return
   */
  private boolean findSomething(String uid, boolean rateOK, boolean sketchOK, boolean descOK,
      PrintWriter out) throws ServletException {
    boolean ret = false;
    // Get a map that says how many of each type we have available.
    Map<String, Integer> potential = getPotential(uid);
    bug("Getting 'anything' for user " + uid + " with potential map: "
        + getPotentialString(potential));
    List<String> ok = new ArrayList<String>();
    int sum = 0;
    for (String k : potential.keySet()) {
      if (potential.get(k) > 0) {
        sum += potential.get(k);
        bug("Sum is now " + sum);
        ok.add(k); // when user says they want to sketch, the request must be serviced by
        // descHandler, and the opposite for when they want to describe.
      }
    }
    if (!rateOK) {
      ok.remove("rate");
      sum -= potential.get("rate");
      bug("Reduced sum (user does not want to rate). it is now " + sum);
    }
    if (!sketchOK) {
      ok.remove("describe"); // remember 'i want to sketch' means 'give me a description'
      sum -= potential.get("describe");
      bug("Reduced sum (user does not want to sketch). it is now " + sum);
    }
    if (!descOK) {
      ok.remove("sketch"); // remember 'i want to describe' means 'give me a sketch'
      sum -= potential.get("sketch");
      bug("Reduced sum (user does not want to describe). it is now " + sum);
    }
    if (sum <= 0) {
      throw new RuntimeException("No more tasks for user " + uid);
    }

    bug("OK list for user " + uid + " is: " + Debug.num(ok, " "));
    if (ok.size() > 0) {
      // We can service the user's preferred category or categories. Yay.
      String whichType = pickOne(ok);
      Object[] giveUserResult = giveUser(whichType, uid, out);
      // index 0: true/false indicating success
      // index 1: String of the todo ID.
      // index 2: String of the object (sketch/description) ID.
      ret = (Boolean) giveUserResult[0];
      removeTodo((String) giveUserResult[2], (String) giveUserResult[1]);
    }
    bug("findSomething(" + uid + ", " + rateOK + ", " + sketchOK + ", " + descOK
        + ", out) returns " + ret);
    return ret;
  }

  private String pickOne(List<String> ok) {
    String ret = null;
    // in cases where the user is OK with rating things, give that a much higher chance
    if (ok.contains("rate") && entropy.nextDouble() > (1.0 - RATE_PROBABILITY)) {
      ret = "rate";
    } else {
      // give the user one of the things in their wishlist (which might be rating anyway, so the
      // above probability is not accurate.
      int which = entropy.nextInt(ok.size());
      bug("which is " + which);
      ret = ok.get(which);
    }
    return ret;
  }

  private void removeTodo(String kind, String todoID) throws ServletException {
    String sql = "delete from todo_" + kind + " where id = ?";
    dbThing.update(sql, todoID);
    bug("Removed rows (maybe) from todo_" + kind + " with ID " + todoID);
  }

  /**
   * Return list has multiple values: Index 0: the todo item's ID (the ID of a todo_rate,
   * todo_sketch, todo_describe entry) Index 1: the ID of the targeted object (a sketch ID or a
   * description ID)
   */
  private List<String> getRandomTodo(String kind, String userID) throws ServletException {
    List<String> ret = new ArrayList<String>();
    String targetIDName = kind + "_id";
    if (kind.equals("rate")) {
      targetIDName = "sketch_id";
    }
    String sql = "select count(id) from todo_" + kind + " where user_id = ?";
    int count = dbThing.getInt(sql, userID);
    int which = entropy.nextInt(count);
    sql = "select id, " + targetIDName + " from todo_" + kind + " where user_id = ? order by "
        + targetIDName + " desc limit ?,1";
    Map<String, String> vals = dbThing.getRow(sql, userID, "" + which);
    ret.add(vals.get("id"));
    ret.add(vals.get(targetIDName));
    return ret;
  }

  private String getPotentialString(Map<String, Integer> m) {
    StringBuffer buf = new StringBuffer();
    for (String s : m.keySet()) {
      buf.append("{" + s + " => " + m.get(s) + "} ");
    }
    return buf.toString().trim();
  }

  /*
   * Returns a map indicating how many items the user can do. The keys are "rate", "sketch" and
   * "describe", and the values are integers in String form.
   */
  protected Map<String, Integer> getPotential(String uid) {
    String suid = "" + uid;
    Map<String, Integer> ret = new HashMap<String, Integer>();
    String rateSql = "SELECT count(id) FROM todo_rate where user_id = ?";
    String sketchSql = "SELECT count(id) FROM todo_sketch where user_id = ?";
    String describeSql = "SELECT count(id) FROM todo_describe where user_id = ?";
    try {
      int howMany = dbThing.getInt(rateSql, suid);
      ret.put("rate", howMany);
      howMany = dbThing.getInt(sketchSql, suid);
      ret.put("describe", howMany); // key of 'describe' is correct
      howMany = dbThing.getInt(describeSql, suid);
      ret.put("sketch", howMany); // key of 'sketch' is correct
    } catch (ServletException ex) {
      ex.printStackTrace();
    }
    return ret;
  }

  private Object[] giveUser(String type, String uid, PrintWriter out) throws ServletException {
    boolean happy = false;
    Object[] ret = new Object[] {
        false, null, null
    };
    List<String> someIDs;
    String kind = "unknown_table";
    if (type.equals("rate")) {
      someIDs = getRandomTodo("rate", uid); // 0: todoID, 1: sketchID
      kind = "rate";
      if (finder.mayRate(someIDs.get(1), uid)) {
        bug("It is ok for user " + uid + " to rate sketch " + someIDs.get(1));
        happy = rateReqHandler.handle(someIDs.get(0), someIDs.get(1), uid, out);
      } else {
        bug("It is no longer ok for user " + uid + " to rate sketch " + someIDs.get(1));
      }
    } else if (type.equals("sketch")) {
      someIDs = getRandomTodo("describe", uid); // 0: todoID, 1: descriptionID
      kind = "describe";
      if (finder.mayDescribe(someIDs.get(1), uid)) {
        bug("It is ok for user " + uid + " to make a description based on sketch " + someIDs.get(1));
        happy = sketchReqHandler.handle(someIDs.get(0), someIDs.get(1), uid, out);
      } else {
        bug("It no longer is ok for user " + uid + " to make a description based on sketch "
            + someIDs.get(1));
      }
    } else { // (type.equals("describe"))
      someIDs = getRandomTodo("sketch", uid); // 0: todoID, 1: sketchID
      kind = "sketch";
      if (finder.maySketch(someIDs.get(1), uid)) {
        bug("It is ok for user " + uid + " to make a sketch based on description " + someIDs.get(1));
        happy = descReqHandler.handle(someIDs.get(0), someIDs.get(1), uid, out);
      } else {
        bug("It is no longer ok for user " + uid + " to make a sketch based on description "
            + someIDs.get(1));
      }
    }
    ret[0] = happy;
    ret[1] = someIDs.get(0);
    ret[2] = kind; // god damn I wish Java supported tuple returns
    return ret;
  }

}
